Computing technology has transformed the way we work and play. Computing systems now take on a wide variety of forms including desktop computers, laptop computers, tablet PCs, Personal Digital Assistance (PDAs), household devices and the like. In its most basic form, a computing system includes system memory and one or more processors. Software in the system memory may be executed by the processors to direct the other hardware of the computing system to perform desired functions.
Software is created by human programmers most often using source code. Source code includes language and symbols that are relatively intuitive to a human programmer. Source code, however, is not directly interpretable by a processor. Accordingly, in order to obtain functioning software, the source code needs to be compiled or otherwise converted to machine code, i.e., binary instructions. The binary instructions can then be loaded and executed to directly instruct the processor on what specific actions are to be preformed in order to enable the functionality of the software.
Software is becoming more and more complex as time progresses. It is not at all unusual for an application to contain many thousands, and even millions, of distinct binary instructions. This rapid advancement in software development is enabled largely by what is commonly referred to as “object oriented” or “attribute-based” programming. Once a component or object is built that performs a particular lower level task, the functionality of that component may be incorporated into many different applications, even if the higher level functionality of the applications are quite different. Accordingly, a component or object becomes a discrete set of computer-executable instructions that, when executed, implement one or more of the functions. The component may be called by other software components or applications and thus provides a programmer access to a wide variety of components that they may incorporate into an application by placing a function call that properly interfaces with the component.
This model of object oriented programming provides a great deal of flexibility in the behavior of a program not traditionally possible when source code is compiled directly into machine code. For example, custom attributes add metadata to give members extra information that extends the definition of a data type's behavior. The attribute values are determined by a programmer at design time, and can be reconfigured at runtime by users and other programs without the need for code changes or recompilation. In fact, more and more programmers are turning to managed languages that utilize a virtual machine (e.g., in a Common Language Runtime CLR environment) to load and parse through intermediate executable instructions and metadata, thereby forming a layer of abstraction between the file format and in memory runtime representations of an object. This general trend has also motivated programmers to move to an even more extensible and flexible model known as scripting, which allows the source code to be dynamically compiled at runtime without the need for an intermediate language on disk.
With the growing popularity of managed code and dynamic compilation, it is often desirable to provide a reflection feature that allows an application or developer to reflect on portions of an application at runtime. These reflection application program interfaces (APIs) allow for such things as: determining the members of an object; getting information about a member's types, fields, methods, constructors, attributes, etc.; finding out what constants and method declarations belong to an interface; creating an instance of a member whose name is not known until runtime; and many other functions. In essence, reflections allow applications to query metadata, to load types and code, understand the surrounding members, make some decisions about them, and execute-all within the safety of the managed runtime.
Although these reflection features provide a great deal of functionality, they still have several deficiencies and drawbacks. For example, reflection can be divided up into two basic functions: getting static information about a member (inspection) and invocation on a member (execution). There are generally two places that reflection APIs look to get information about a member (i.e., a type, method, field, property, event, attribute, constructor, etc.) to create managed data, these include: metadata, and runtime data structures. Metadata is a typically an “on disk” or file format set of tables that describe members and the entities or elements contained in those members. Runtime data structures, on the other hand, typically only require information from the metadata that is constantly and consistently referenced by runtime services (e.g., just-in-time (JIT) compilation, security checks, etc.). A code execution manager or virtual machine will lazily populate the data structures with information from metadata to get the currently executing job done. As such, the information within the runtime data structures is usually a different and more compact subset of the metadata in file format or on disk.
In order to create a member info, reflection must use both the metadata and the runtime data structures. Touching any sort of metadata on disk, however, is expensive, and slows the overall performance of the system. Further, reflection typically has a very strict policy when it comes to caching members. As such, member info caches that are built in the managed part of the virtual machine simply consume more and more unmanaged memory and resources as more members are reflected upon and unnecessary information from parts of the member info structure that are never needed are continually stored. In addition, the reflection member info cache is never reclaimed. Accordingly, the more members that are reflected on, the bigger the cache grows even if an application is done with the reflection feature.
Another drawback of current reflection mechanisms also lies in the invocation of a member. Within the invocation domain, there are essentially early, late, and hybrid bound invocations. Early-bound calls are emitted by the compiler, whereas late-bound and hybrid calls are, at least partially, performed and analyzed at runtime instead of compile time. As such, it's easy to see that early-bound cases are significantly faster than their late-bound and hybrid counter parts. This is due to the fact that the late-bound and hybrid cases typically have several functions that must be performed during the runtime execution. For example, when invoking a member, the virtual machine must figure out the exact member to invoke using a string input, and perform checks to make sure the invocation is safe. Since a string is used to determine the member, the binding logic alone is expensive and can lead to collisions in names. Further, reflection uses the member info to compare against other member infos when determining if a member is safe to invoke. As such, the drawbacks described above regarding performance and working sets is additionally compounded when multiple comparisons are needed.